from hwt.doc_markers import internal
from hwt.hdl.types.hdlType import HdlType
from hwt.hdl.types.structValBase import HStructConstBase
from hwt.serializer.generic.indent import getIndent


class HStructFieldMeta():
    """
    Meta for field in struct type

    :ivar ~.split: flag which specifies if structured data type of this field
        should be synchronized as a one interface
        or each it's part should be synchronized separately
    """

    def __init__(self, split=False):
        self.split = split

    def __eq__(self, other):
        if other is None:
            return False
        return self.split == other.split

    @internal
    def __hash__(self):
        return hash(self.split)


class HStructField(object):

    def __init__(self, typ: HdlType, name: str, meta=None):
        assert isinstance(name, str) or name is None, name
        assert isinstance(typ, HdlType), typ
        self.name = name
        self.dtype = typ
        self.meta = meta

    def __eq__(self, other):
        return self.name == other.name and\
               self.dtype == other.dtype and\
               self.meta == other.meta

    def __hash__(self):
        return hash((self.name, self.dtype, self.meta))

    def __repr__(self):
        name = self.name
        if name is None:
            name = "<padding>"

        return f"<HStructField {self.dtype}, {name:s}>"


protectedNames = {"clone", "staticEval", "from_py", "_dtype"}


class HStruct(HdlType):
    """
    HDL structure type

    :ivar ~.fields: tuple of :class:`~.HStructField` instances in this struct
    :ivar ~.name: name of this HStruct type
    :ivar ~.field_by_name: dictionary which maps the name of the field to :class:`~.HStructField` instance
    :ivar ~._constCls: Class of value for this type as usual
        in HdlType implementations
    """

    def __init__(self, *template, name=None, const=False):
        """
        :param template: list of tuples (type, name) or :class:`~.HStructField` objects
            name can be None (= padding)
        :param name: optional name used for debugging purposes
        """
        super(HStruct, self).__init__(const=const)

        fields = []
        field_by_name = {}
        self.name = name
        bit_length = 0
        for f in template:
            try:
                field = HStructField(*f)
            except TypeError:
                field = f
            if not isinstance(field, HStructField):
                raise TypeError(f"Template for struct field {f} is"
                                " not in valid format")

            fields.append(field)
            if field.name is not None:
                assert field.name not in field_by_name, field.name
                field_by_name[field.name] = field

            t = field.dtype
            if bit_length is not None:
                try:
                    _bit_length = t.bit_length()
                    bit_length += _bit_length
                except TypeError:
                    bit_length = None

        self.fields = tuple(fields)
        self.field_by_name = field_by_name
        self.__hash = hash((self.name, self.const, self.fields))
        self.__bit_length_val = bit_length

        usedNames = set(field_by_name.keys())
        assert not protectedNames.intersection(usedNames), \
            protectedNames.intersection(usedNames)

        class StructConst(HStructConstBase):
            __slots__ = list(usedNames)

        if name is not None:
            StructConst.__name__ = name + "Val"

        self._constCls = StructConst

    def bit_length(self):
        bl = self.__bit_length_val
        if bl is None:
            raise TypeError("Can not request bit_lenght on type"
                            " which has not fixed size")
        else:
            return self.__bit_length_val

    @internal
    def getConstCls(self):
        return self._constCls

    @internal
    @classmethod
    def get_reinterpret_cast_fn(cls):
        from hwt.hdl.types.structCast import hstruct_reinterpret
        return hstruct_reinterpret

    @internal
    def __fields__eq__(self, other):
        if len(self.fields) != len(other.fields):
            return False
        for sf, of in zip(self.fields, other.fields):
            if (sf.name != of.name or
                    sf.dtype != of.dtype or
                    sf.meta != of.meta):
                return False
        return True

    def __eq__(self, other):
        if self is other:
            return True
        if (type(self) is type(other)):
            if self.name != other.name or self.const != other.const:
                return False
            try:
                self_l = self.bit_length()
            except TypeError:
                self_l = -1
            try:
                other_l = other.bit_length()
            except TypeError:
                other_l = -1

            return self_l == other_l and self.__fields__eq__(other)
        return False

    @internal
    def __hash__(self):
        return self.__hash

    def __add__(self, other):
        """
        override of addition, merge struct into one
        """
        assert isinstance(other, HStruct)
        return HStruct(*self.fields, *other.fields)

    def __repr__(self, indent=0, withAddr=None, expandStructs=False):
        """
        :param indent: number of indentation
        :param withAddr: if is not None is used as a additional
            information about on which address this type is stored
            (used only by HStruct)
        :param expandStructs: expand HStructTypes (used by HStruct and HArray)
        """
        if self.name:
            name = self.name + " "
        else:
            name = ""

        myIndent = getIndent(indent)
        childIndent = getIndent(indent + 1)
        header = f"{myIndent:s}struct {name:s}{{"

        buff = [header, ]
        for f in self.fields:
            if withAddr is not None:
                withAddr_B = withAddr // 8
                addrTag = f" // start:0x{withAddr:x}(bit) 0x{withAddr_B:x}(byte)"
            else:
                addrTag = ""

            if f.name is None:
                buff.append(f"{childIndent:s}//{f.dtype} empty space{addrTag:s}")
            else:
                buff.append("%s %s%s" % (
                               f.dtype.__repr__(indent=indent + 1,
                                                withAddr=withAddr,
                                                expandStructs=expandStructs),
                            f.name, addrTag))
            if withAddr is not None:
                withAddr += f.dtype.bit_length()

        buff.append(f"{myIndent:s}}}")
        return "\n".join(buff)
